🎖️GitЯра🎖️
Commit 60b7908e1bfeb63eee3dcf6df655da4adb72e6c0
Parents : b692670
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-06-23T13:34:58-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-06-23T18:34:58Z
feat(firmware): link OTAFIX bootloader from slow-DFU success screen (#5917)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Changes
8 files changed, 46 insertions(+), 16 deletions(-)
Diff
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
index 76e74ebff7..d85d0acd6b 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
@@ -112,6 +112,7 @@ import org.meshtastic.core.resources.firmware_update_release_notes
import org.meshtastic.core.resources.firmware_update_retry
import org.meshtastic.core.resources.firmware_update_save_dfu_file
import org.meshtastic.core.resources.firmware_update_select_file
+import org.meshtastic.core.resources.firmware_update_slow_bootloader_hint
import org.meshtastic.core.resources.firmware_update_source_local
import org.meshtastic.core.resources.firmware_update_stable
import org.meshtastic.core.resources.firmware_update_success
@@ -150,6 +151,14 @@ import org.meshtastic.core.ui.util.rememberSaveFileLauncher
private const val CYCLE_DELAY_MS = 4500L
+/**
+ * Flashing instructions for the OTAFIX 2.1 bootloader, which lifts the BLE DFU MTU cap (20-byte → 244-byte packets,
+ * ~10× faster updates). Offered on the Success screen after a low-speed transfer — never mid-upload, where leaving the
+ * app could drop the DFU link and brick the device.
+ */
+private const val OTAFIX_BOOTLOADER_URL =
+ "https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX#changes-in-otafix-21"
+
@Composable
@Suppress("LongMethod")
fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateViewModel, modifier: Modifier = Modifier) {
@@ -328,7 +337,8 @@ private fun FirmwareUpdateContent(
is FirmwareUpdateState.Error -> ErrorState(error = state.error, onRetry = actions.onRetry)
- is FirmwareUpdateState.Success -> SuccessState(onDone = actions.onDone)
+ is FirmwareUpdateState.Success ->
+ SuccessState(onDone = actions.onDone, wasLowSpeedTransfer = state.wasLowSpeedTransfer)
is FirmwareUpdateState.AwaitingFileSave -> AwaitingFileSaveState(state, actions.onSaveFile)
}
@@ -866,8 +876,9 @@ internal fun ErrorState(error: UiText, onRetry: () -> Unit) {
}
@Composable
-internal fun SuccessState(onDone: () -> Unit) {
+internal fun SuccessState(wasLowSpeedTransfer: Boolean = false, onDone: () -> Unit) {
val haptic = LocalHapticFeedback.current
+ val openUrl = rememberOpenUrl()
LaunchedEffect(Unit) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -884,6 +895,18 @@ internal fun SuccessState(onDone: () -> Unit) {
style = MaterialTheme.typography.headlineLarge,
textAlign = TextAlign.Center,
)
+ // The just-finished transfer was MTU-capped (stock bootloader). Now that the device is back and it's safe to
+ // leave the app, offer a one-time OTAFIX upgrade tip for faster future updates.
+ if (wasLowSpeedTransfer) {
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(Res.string.firmware_update_slow_bootloader_hint),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ )
+ TextButton(onClick = { openUrl(OTAFIX_BOOTLOADER_URL) }) { Text(stringResource(Res.string.learn_more)) }
+ }
Spacer(Modifier.height(32.dp))
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
val largeHeight = ButtonDefaults.LargeContainerHeight
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
index 1cb212b084..893dcaae2b 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
@@ -72,8 +72,13 @@ sealed interface FirmwareUpdateState {
/** An error occurred at any stage of the update pipeline. */
data class Error(val error: UiText) : FirmwareUpdateState
- /** The firmware update completed and the device reconnected successfully. */
- data object Success : FirmwareUpdateState
+ /**
+ * The firmware update completed and the device reconnected successfully.
+ *
+ * @property wasLowSpeedTransfer True if the upload ran at the MTU-capped low speed (stock bootloader), so the
+ * Success screen can offer a one-time OTAFIX upgrade tip for faster future updates.
+ */
+ data class Success(val wasLowSpeedTransfer: Boolean = false) : FirmwareUpdateState
/** UF2 file is ready; waiting for the user to choose a save location (USB flow). */
data class AwaitingFileSave(val uf2Artifact: FirmwareArtifact, val fileName: String) : FirmwareUpdateState
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
index c2555c9ad8..7334a7af93 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
@@ -237,8 +237,9 @@ class FirmwareUpdateViewModel(
updateState = { _state.value = it },
)
- when (_state.value) {
- is FirmwareUpdateState.Success -> verifyUpdateResult(originalDeviceAddress)
+ when (val finalState = _state.value) {
+ is FirmwareUpdateState.Success ->
+ verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer)
is FirmwareUpdateState.Error -> {
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
@@ -325,8 +326,9 @@ class FirmwareUpdateViewModel(
)
tempFirmwareFile = updateArtifact ?: extractedFile
- when (_state.value) {
- is FirmwareUpdateState.Success -> verifyUpdateResult(originalDeviceAddress)
+ when (val finalState = _state.value) {
+ is FirmwareUpdateState.Success ->
+ verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer)
is FirmwareUpdateState.Error -> {
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
@@ -355,7 +357,7 @@ class FirmwareUpdateViewModel(
}
}
- private suspend fun verifyUpdateResult(address: String?) {
+ private suspend fun verifyUpdateResult(address: String?, wasLowSpeedTransfer: Boolean = false) {
_state.value = FirmwareUpdateState.Verifying
// Trigger a fresh connection attempt by MeshService using the original prefixed address
@@ -378,7 +380,7 @@ class FirmwareUpdateViewModel(
Logger.w { "Post-update verification timed out for $address" }
_state.value = FirmwareUpdateState.VerificationFailed
} else {
- _state.value = FirmwareUpdateState.Success
+ _state.value = FirmwareUpdateState.Success(wasLowSpeedTransfer)
}
}
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
index 9126b8db11..bc48c34557 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
@@ -351,6 +351,6 @@ class Esp32OtaUpdateHandler(
.getOrThrow()
Logger.i { "ESP32 OTA: Firmware stream completed" }
- updateState(FirmwareUpdateState.Success)
+ updateState(FirmwareUpdateState.Success())
}
}
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt
index 5a9bd8e290..12592e57cd 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt
@@ -208,7 +208,7 @@ class SecureDfuHandler(
)
completed = true
- updateState(FirmwareUpdateState.Success)
+ updateState(FirmwareUpdateState.Success(wasLowSpeedTransfer = transport.isLowSpeedTransfer))
zipFile
} finally {
// Send ABORT if cancelled mid-transfer, then always clean up.
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
index 3de9ffb661..7f976ff482 100644
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
@@ -129,7 +129,7 @@ class FirmwareUpdateIntegrationTest {
@Suppress("UNCHECKED_CAST")
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
updateState(FirmwareUpdateState.Updating(ProgressState()))
- updateState(FirmwareUpdateState.Success)
+ updateState(FirmwareUpdateState.Success())
null
}
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
index a8eddff838..a19b8e455c 100644
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
@@ -187,7 +187,7 @@ class FirmwareUpdateViewModelTest {
.calls {
@Suppress("UNCHECKED_CAST")
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
- updateState(FirmwareUpdateState.Success)
+ updateState(FirmwareUpdateState.Success())
null
}
diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
index 23a0d03ab2..241cce4dea 100644
--- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
+++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
@@ -197,7 +197,7 @@ class FirmwareUpdateViewModelFileTest {
.calls {
@Suppress("UNCHECKED_CAST")
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
- updateState(FirmwareUpdateState.Success)
+ updateState(FirmwareUpdateState.Success())
null
}
@@ -257,7 +257,7 @@ class FirmwareUpdateViewModelFileTest {
.calls {
@Suppress("UNCHECKED_CAST")
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
- updateState(FirmwareUpdateState.Success)
+ updateState(FirmwareUpdateState.Success())
null
}
Served by rngit 1.4.2 - Generated in 0.18s